// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.
using System;
using System.Cloud.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Actors;
using System.Threading.Tasks;

namespace ActorFramework.Fakebook
{
    /*
     * FakebookPerson is a client-side view of a Fakebook Person actor.  A person represents a user,
     * with a full name, a user name, profile information, and a profile picture.  
     * The person actor also has a list of pending friendship requests.  Asking to make friends
     * goes through the person actor, since it requires user interaction to confirm friendship.
     * 
     * Fakebook relies on several actors:
     *     Person actor - represents a user's name & profile information.
     *     Friends List - a separate CloudList<String>
     *     Photos - a CloudStringDictionary<PictureInfo>
     *     News Posts - a CloudList<String>
     *     NewsFeed - an actor that aggregates your friends' (and your) news posts.  Also subscribes to 
     *         your Friends list so it knows when to subscribe or unsubscribe to a new friend's news posts.
     * 
     * Note: Fakebook has no concept of authentication or security.  So a FakebookPerson class
     * currently gives you access to all data.  There is no distinction between "my" data vs. my
     * read-only view of someone else's public data (without seeing private data).  Fakebook
     * is just a demo showing how to build interesting actors and build a non-trivial system
     * via message passing (both actor to actor and actor to client).
     */
    public class FakebookPerson : IDisposable
    {
        private FabricActorClient _actorClient;
        private String _accountName;
        private String _fabricAddress;
        private bool _connectThroughGateway;
        private CloudStringDictionary<PictureInfo> _photos;
        private ObservableCloudList<String> _friends;
        private CloudList<String> _newsPosts;

        // Local cache - we probably need names a lot
        private String _firstName;
        private String _lastName;
        private PictureInfo _cachedProfilePicture;

        private const String ApplicationType = "fabric:/actor/fakebook";  // Do not put a / on the end here.
        // We create one service instance, then partition it based on the account name.
        private const String PeopleActorName = "fabric:/actor/fakebook/People";
        public static readonly int NumPartitions = 5;

        public FakebookPerson(String fabricAddress, String accountName, bool connectThroughGateway = false, int numReplicas = 2)
        {
            Contract.Requires(numReplicas >= 1);

            Uri actorUri = new Uri(PeopleActorName);
            _accountName = accountName;
            _fabricAddress = fabricAddress;
            _connectThroughGateway = connectThroughGateway;

            _actorClient = FabricActorClient.CreateOrConnectToActor(fabricAddress, ApplicationType, actorUri,
                                                                    "EmptyActorReplica", connectThroughGateway, numReplicas,
                                                                    NumPartitions, accountName);
        }

        public void Dispose()
        {
            Dispose(true);
        }

        protected void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_actorClient != null)
                {
                    _actorClient.Dispose();
                    _actorClient = null;
                }
                if (_photos != null)
                {
                    _photos.Dispose();
                    _photos = null;
                }
                if (_newsPosts != null)
                {
                    _newsPosts.Dispose();
                    _newsPosts = null;
                }
                if (_friends != null)
                {
                    _friends.Dispose();
                    _friends = null;
                }
            }
        }

        public override bool Equals(object obj)
        {
            FakebookPerson other = obj as FakebookPerson;
            if (other == null)
                return false;
            return _accountName == other._accountName;
        }

        public override int GetHashCode()
        {
            return _accountName.GetHashCode();
        }

        public override string ToString()
        {
            return "FakebookPerson: " + _accountName;
        }

        public bool EnsureAccountExists()
        {
            return _actorClient.CallMethod<bool>("TryConstruct", new Object[] { _accountName });
        }

        public void MakeDependentActors()
        {
            // Make NewsFeed Actor.  This has two effects:
            // 1) Creates the actor for future use.  We don't use it here though.
            // 2) Upload the NewsFeed actor's assembly from the client to the actor.
            NewsFeed newsFeed = new NewsFeed(_fabricAddress, _accountName, _connectThroughGateway);
            newsFeed.Dispose();
        }

        // Deletes the person and all related actors.
        public void DeletePerson()
        {
            // @TODO: Figure out how to delete a partitioned Person actor.
            //FabricActorClient.DeleteActor(_fabricAddress, "fabric:/actor/fakebook/" + _accountName);

            // Can't use the property - don't need an ObservableCloudList, nor do we have the right synchronization context.
            CloudList<String> friendsList = _friends ?? new CloudList<String>(_fabricAddress, "Friends", _accountName, NumPartitions, _connectThroughGateway);
            friendsList.Destroy();

            CloudStringDictionary<PictureInfo> photos = Photos;
            photos.Destroy();

            CloudList<String> newsPosts = NewsPosts;
            newsPosts.Destroy();

            // @TODO: Figure out how to delete a partitioned NewsFeed actor.
            //FabricActorClient.DeleteActor(_fabricAddress, NewsFeed.GetNewsfeedActorName(_accountName));

            Dispose();
        }

        public static void DeletePerson(String accountName, String fabricAddress, bool connectThroughGateway)
        {
            List<Exception> exceptions = new List<Exception>();

            try
            {
                // @TODO: Make this work well with partitioning.
                //FabricActorClient.DeleteActor(fabricAddress, PeopleActorName, partitionName: accountName);
            }
            catch (Exception e) {
                exceptions.Add(e);
            }

            try
            {
                CloudList<String> friendsList = new CloudList<string>(fabricAddress, "Friends", accountName, NumPartitions, connectThroughGateway);
                friendsList.Destroy();
            }
            catch (Exception e)
            {
                exceptions.Add(e);
            }

            try
            {
                CloudStringDictionary<PictureInfo> photos = new CloudStringDictionary<PictureInfo>(fabricAddress, "Photos", accountName, NumPartitions, connectThroughGateway);
                photos.Destroy();
            }
            catch (Exception e)
            {
                exceptions.Add(e);
            }

            try
            {
                CloudList<String> newsPosts = new CloudList<String>(fabricAddress, "NewsPosts", accountName, NumPartitions, connectThroughGateway);
                newsPosts.Destroy();
            }
            catch (Exception e)
            {
                exceptions.Add(e);
            }

            try {
                // @TODO: Make this work well with partitioning
                //FabricActorClient.DeleteActor(fabricAddress, NewsFeed.GetNewsfeedActorName(accountName));
            }
            catch (Exception e)
            {
                exceptions.Add(e);
            }

            if (exceptions.Count > 0)
            {
                if (exceptions.Count == 1)
                    throw exceptions[0];
                else
                    throw new AggregateException(exceptions.ToArray());
            }
        }


        public static String GetAccountName(String firstName, String lastName)
        {
            Contract.Requires(!String.IsNullOrWhiteSpace(firstName));
            Contract.Requires(!String.IsNullOrWhiteSpace(lastName));

            return firstName + "." + lastName;
        }

        public FabricActorClient GetFabricActorClient()
        {
            return _actorClient;
        }

        public String AccountName {
            get { return _accountName; }
        }

        public String FirstName {
            get {
                if (_firstName == null)
                    _firstName = _actorClient.CallMethod<String>("GetProfileInfo", new Object[] { _accountName, "FirstName" });
                return _firstName;
            }
            set {
                _actorClient.CallMethod("SetProfileInfo", new Object[] { _accountName, "FirstName", value });
                _firstName = value;
            }
        }

        public String LastName {
            get {
                if (_lastName == null)
                    _lastName = _actorClient.CallMethod<String>("GetProfileInfo", new Object[] { _accountName, "LastName" });
                return _lastName;
            }
            set {
                _actorClient.CallMethod("SetProfileInfo", new Object[] { _accountName, "LastName", value });
                _lastName = value;
            }
        }

        public String FullName {
            get { return FirstName + " " + LastName; }
        }

        public Gender Gender {
            get { return _actorClient.CallMethod<Gender?>("GetProfileInfo", new Object[] { _accountName, "Gender" }).GetValueOrDefault(); }
            set { _actorClient.CallMethod("SetProfileInfo", new Object[] { _accountName, "Gender", value }); }
        }

        public String City {
            get { return _actorClient.CallMethod<String>("GetProfileInfo", new Object[] { _accountName, "City" }); }
            set { _actorClient.CallMethod("SetProfileInfo", new Object[] { _accountName, "City", value }); }
        }

        public DateTimeOffset? Birthday
        {
            get { return _actorClient.CallMethod<DateTimeOffset?>("GetProfileInfo", new Object[] { _accountName, "Birthday" }); }
            set { _actorClient.CallMethod("SetProfileInfo", new Object[] { _accountName, "Birthday", value }); }
        }

        public RelationshipStatus RelationshipStatus
        {
            get {
                // Note the creative use of Nullable<RelationshipStatus> here to represent unspecified values.  Using 
                // Nullable<T> works out well with our JSON serializer.  For this type of optional value, it's the best 
                // fit for value types, including enums.
                var ret = _actorClient.CallMethod<RelationshipStatus?>("GetProfileInfo", new Object[] { _accountName, "RelationshipStatus" });
                return ret.HasValue ? ret.Value : RelationshipStatus.Unspecified;
            }
            set { _actorClient.CallMethod("SetProfileInfo", new Object[] { _accountName, "RelationshipStatus", value }); }
        }

        public PictureInfo ProfilePicture
        {
            get {
                if (_cachedProfilePicture == null)
                {
                    PictureInfo picture = _actorClient.CallMethod<PictureInfo>("GetProfileInfo", new Object[] { _accountName, "ProfilePicture" });
                    if (picture == null)
                    {
                        Stream s = typeof(FakebookPerson).Assembly.GetManifestResourceStream("ActorFramework.Fakebook.PersonNoPicture.png");
                        byte[] bytes = new byte[s.Length];
                        s.Read(bytes, 0, bytes.Length);
                        picture = new PictureInfo("No Picture", bytes, null, null, null, null);
                    }
                    _cachedProfilePicture = picture;
                }
                return _cachedProfilePicture;
            }
            set { _actorClient.CallMethod("SetProfileInfo", new Object[] { _accountName, "ProfilePicture", value }); }
        }

        #region Friend Requests

        public void MakeFriendRequest(String potentialFriendName)
        {
            _actorClient.CallMethod("MakeFriendRequest", new Object[] { _accountName, potentialFriendName });
        }

        // For programmatic testing
        public void ForceMakeFriends(IList<String> friendNames)
        {
            Contract.Requires(friendNames != null);
            Contract.Requires(friendNames.Count > 0);
            String[] parameters = new String[1 + friendNames.Count];
            parameters[0] = _accountName;
            friendNames.CopyTo(parameters, 1);
            _actorClient.CallMethod("ForceMakeFriends", parameters);
        }

        public Task ForceMakeFriendsAsync(IList<String> friendNames)
        {
            Contract.Requires(friendNames != null);
            Contract.Requires(friendNames.Count > 0);
            String[] parameters = new String[1 + friendNames.Count];
            parameters[0] = _accountName;
            friendNames.CopyTo(parameters, 1);
            return _actorClient.CallMethodAsyncVoid("ForceMakeFriends", parameters);
        }

        public void AcceptFriendRequest(String friendName)
        {
            _actorClient.CallMethod("AcceptFriendRequest", new Object[] { _accountName, friendName });
        }

        public void RejectFriendRequest(String friendName, String message="This person doesn't want to be your friend.")
        {
            _actorClient.CallMethod("RejectFriendRequest", new Object[] { _accountName, friendName, message });
        }

        public void Unfriend(String exFriendName)
        {
            _actorClient.CallMethod("Unfriend", new Object[] { _accountName, exFriendName });
        }

        #endregion Friend Requests

        public void AddPhoto(PictureInfo picture)
        {
            Contract.Requires(picture != null);
            if (_photos == null)
                _photos = new CloudStringDictionary<PictureInfo>(_fabricAddress, "Photos", _accountName, NumPartitions, _connectThroughGateway);

            _photos[picture.Title] = picture;
        }

        public CloudStringDictionary<PictureInfo> Photos {
            get {
                if (_photos == null)
                    _photos = new CloudStringDictionary<PictureInfo>(_fabricAddress, "Photos", _accountName, NumPartitions, _connectThroughGateway);
                return _photos;
            }
        }

        public ObservableCloudList<String> GetFriendsList(SynchronizationContext uiThreadSyncContext)
        {
            if (_friends == null)
                _friends = new ObservableCloudList<string>(_fabricAddress, "Friends", _accountName, NumPartitions, uiThreadSyncContext, _connectThroughGateway);
            return _friends;
        }

        public CloudList<String> NewsPosts {
            get
            {
                if (_newsPosts == null)
                    _newsPosts = new CloudList<string>(_fabricAddress, "NewsPosts", _accountName, NumPartitions, _connectThroughGateway);
                return _newsPosts;
            }
        }

        public IDisposable Subscribe(String eventType, IObserver<ActorEvent> eventObserver)
        {
            return _actorClient.Subscribe(eventType, eventObserver);
        }
    }
}
